﻿using System;
using System.Diagnostics;

namespace SfsTools.Core
{
    /// <summary>
    /// Validates the index area.
    /// </summary>
    internal class IndexAreaValidatorVisitor : Object, IIndexEntryVisitor
    {
        #region Properties

        /// <summary>
        /// Contains the index area.
        /// </summary>
        private readonly IndexArea FIndexArea;
        /// <summary>
        /// Contains the data area.
        /// </summary>
        private readonly DataArea FDataArea;
        /// <summary>
        /// Contains the index of the current entry.
        /// </summary>
        private int FCurrent;
        /// <summary>
        /// Contains the boot block.
        /// </summary>
        private readonly BootBlock FBootBlock;
        /// <summary>
        /// Contains the expected count of continuation entries.
        /// </summary>
        private int FExpectedContinuationEntries;

        #endregion

        #region Methods

        /// <summary>
        /// Performs the validation.
        /// </summary>
        /// <exception cref="InvalidIndexAreaException">The index area failed validation.</exception>
        //[DebuggerStepThrough]
        public void Validate()
        {
            for (int i = 0; i < FIndexArea.Length; i++)
            {
                FCurrent = i;
                IndexEntry entry = FIndexArea[i];
                if (entry != null)
                    entry.AcceptVisitor(this);
            }
            // Allocate leftover blocks from reserved area and index area, preventing their use.
            FDataArea.ReserveRegion(new BlockRegion(0, FBootBlock.FirstDataAreaBlock));
            FDataArea.ReserveRegion(new BlockRegion(FBootBlock.FirstIndexAreaBlock, FBootBlock.IndexAreaBlockCount));
        }

        /// <summary>
        /// Tries to allocate a data area block region.
        /// </summary>
        /// <param name="start">The starting block number.</param>
        /// <param name="length">The count of blocks to allocate.</param>
        /// <exception cref="OverlappingFilesFoundException">An attempt was made to allocate an overlapping region.</exception>
        private void Allocate(long start, long length)
        {
            try
            {
                FDataArea.AllocateRegionForValidation(new BlockRegion(start, length));
            }
            catch (InvalidAllocationException ex)
            {
                throw new OverlappingFilesFoundException(ex);
            }
        }

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="IndexAreaValidatorVisitor"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="IndexAreaValidatorVisitor"/>.
        /// </summary>
        /// <param name="bootblock">The boot block.</param>
        /// <param name="index">The index area.</param>
        /// <param name="data">The data area.</param>
        [DebuggerStepThrough]
        public IndexAreaValidatorVisitor(BootBlock bootblock, IndexArea index, DataArea data)
            : base()
        {
            FIndexArea = index;
            FDataArea = data;
            FBootBlock = bootblock;
        }
        #endregion

        #region IIndexEntryVisitor Members

        /// <summary>
        /// Called by <see cref="BadSectorEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitBadSector(BadSectorEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.BadSector);
            // First block number must be valid.
            if (entry.FirstBlock < 0)
                throw new AllocatedBlockRegionOutsideDataAreaException();
            // Last block cannot be located before first block.
            if (entry.LastBlock <= entry.FirstBlock) // exclusive
                throw new InvalidBlockRegionAllocationException();
            // Cannot declare a region outside the volume.
            if ((entry.LastBlock > FBootBlock.BlockCountInVolume) || (entry.FirstBlock >= FBootBlock.BlockCountInVolume))
                throw new AllocatedBlockRegionOutsideVolumeException();
            if (entry.FirstBlock < FBootBlock.FirstDataAreaBlock)
            {
                // Bad sectors in reserved area. The specification doesn't
                // say anything about this case. Assume it's legal.
            }
            if (entry.LastBlock >= FBootBlock.FirstIndexAreaBlock)            
            {
                // Bad sectors in the reserved area. The specification doesn't
                // say anything about this case. Assume it's legal.
                // Note: for complexity reasons, this is not supported. It is a limitation of SFStools.
                throw new BadSectorsInIndexAreaNotSupportedException();
            }
            // Try to allocate block region, check for overlapping files.
            Allocate(entry.FirstBlock, entry.LastBlock - entry.FirstBlock); // exclusive
            FExpectedContinuationEntries = 0;
        }

        /// <summary>
        /// Called by <see cref="ContinuationEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitContinuation(ContinuationEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries == 0)
            {
                // Unexpected continuation entry.
                Debug.WriteLine(String.Format("Unexpected continuation entry at {0}, marking as free", FCurrent));
            }
            else
            {
                FExpectedContinuationEntries--;
            }
        }

        /// <summary>
        /// Called by <see cref="PresentDirectoryEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitPresentDirectory(PresentDirectoryEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.PresentDirectory);
            FExpectedContinuationEntries = entry.ContinuationCount;
        }

        /// <summary>
        /// Called by <see cref="PresentFileEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitPresentFile(PresentFileEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.PresentFile);
            // File size cannot be negative.
            if (entry.FileSize < 0)
                throw new NegativeFileSizeException();
            // File size cannot be larger than data area size also.
            if (entry.FileSize > FBootBlock.DataAreaSize)
                throw new FileSizeLargerThanDataAreaException();
            // Cannot declare a region outside the volume.
            if ((entry.LastBlock > FBootBlock.BlockCountInVolume) || (entry.FirstBlock >= FBootBlock.BlockCountInVolume))
                throw new AllocatedBlockRegionOutsideVolumeException();
            // First block number must be valid.
            if (entry.FirstBlock < FBootBlock.FirstDataAreaBlock)
                throw new AllocatedBlockRegionOutsideDataAreaException();
            // Last block cannot be located before first block.
            if (entry.LastBlock <= entry.FirstBlock) // exclusive
                throw new InvalidBlockRegionAllocationException();
            // Last block number must be valid.
            if (entry.LastBlock > FBootBlock.FirstFreeAreaBlock)
                throw new AllocatedBlockRegionOutsideDataAreaException();
            // File size cannot be larger than the size of the allocated block run.
            if (entry.FileSize > (entry.LastBlock - entry.FirstBlock) * FBootBlock.BlockSize) // exclusive
                throw new FileSizeLargerThanAllocatedBlockRegionException();
            // Note: the specification explicitly allows FileSize to be much less than the allocated block run length.
            // Try to allocate block region for file, check for overlapping files.
            Allocate(entry.FirstBlock, entry.LastBlock - entry.FirstBlock); // exclusive
            FExpectedContinuationEntries = entry.ContinuationCount;
        }

        /// <summary>
        /// Called by <see cref="DeletedDirectoryEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitDeletedDirectory(DeletedDirectoryEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.DeletedDirectory);
            FExpectedContinuationEntries = entry.ContinuationCount;
        }

        /// <summary>
        /// Called by <see cref="DeletedFileEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitDeletedFile(DeletedFileEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.DeletedFile);
            // File size cannot be negative.
            if (entry.FileSize < 0)
                throw new NegativeFileSizeException();
            // File size cannot be larger than data area size also.
            if (entry.FileSize > FBootBlock.DataAreaSize)
                throw new FileSizeLargerThanDataAreaException();
            // Cannot declare a region outside the volume.
            if ((entry.LastBlock > FBootBlock.BlockCountInVolume) || (entry.FirstBlock >= FBootBlock.BlockCountInVolume))
                throw new AllocatedBlockRegionOutsideVolumeException();
            // First block number must be valid.
            if (entry.FirstBlock < FBootBlock.FirstDataAreaBlock)
                throw new AllocatedBlockRegionOutsideDataAreaException();
            // Last block cannot be located before first block.
            if (entry.LastBlock <= entry.FirstBlock) // exclusive
                throw new InvalidBlockRegionAllocationException();
            // Last block number must be valid.
            if (entry.LastBlock > FBootBlock.FirstFreeAreaBlock)
                throw new AllocatedBlockRegionOutsideDataAreaException();
            // File size cannot be larger than the size of the allocated block run.
            if (entry.FileSize > (entry.LastBlock - entry.FirstBlock) * FBootBlock.BlockSize) // exclusive
                throw new FileSizeLargerThanAllocatedBlockRegionException();
            // Note: the specification explicitly allows FileSize to be much less than the allocated block run length.
            // Do not allocate block region for file, as the file may have already been overwritten,
            // and there is just no point checking for that. The Resurrect method of IFile
            // will check for this condition anyway.
            FExpectedContinuationEntries = entry.ContinuationCount;
        }

        /// <summary>
        /// Called by <see cref="StartingMarkerEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitStartingMarker(StartingMarkerEntry entry)
        {
            // Must be first entry.
            if (FCurrent != 0)
                throw new UnexpectedStartingMarkerException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.StartingMarker);
        }

        /// <summary>
        /// Called by <see cref="UnknownIndexEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitUnknown(UnknownIndexEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.Unknown);
            // While the specification states that any unknown entry must be treated
            // as an unused entry, I think it is unwise and will manifest in forms
            // of compatibility issues with anything never than 1.0.
            FIndexArea.ClearEntry(FCurrent);
            if (FBootBlock.VersionNumber > 0x10)
            {
                Debug.WriteLine("Unknown entry found and FS version is > 1.0, reverting to 1.0");
                // Revert version.
                FBootBlock.VersionNumber = 0x10;
            }
        }

        /// <summary>
        /// Called by <see cref="UnusedIndexEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitUnused(UnusedIndexEntry entry)
        {
            // Cannot be first or last entry.
            if ((FCurrent == 0) || (FCurrent >= FIndexArea.Length - 1))
                throw new InvalidIndexAreaException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.Unused);
        }

        /// <summary>
        /// Called by <see cref="VolumeIdentifierEntry"/> objects.
        /// </summary>
        /// <param name="entry">The entry to visit.</param>
        [DebuggerStepThrough]
        void IIndexEntryVisitor.VisitVolumeIdentifier(VolumeIdentifierEntry entry)
        {
            // Must be last entry.
            if (FCurrent != FIndexArea.Length - 1)
                throw new UnexpectedVolumeIdentifierException();
            if (FExpectedContinuationEntries > 0)
                throw new MissingContinuationEntryException(IndexAreaEntryType.VolumeIdentifier);
        }

        #endregion
    }
}
